Skip to content

Fix down arrow key not working after font size change#364

Open
objectiveous wants to merge 2 commits intoCodeEditApp:mainfrom
objectiveous:fix/arrow-keys-after-font-change
Open

Fix down arrow key not working after font size change#364
objectiveous wants to merge 2 commits intoCodeEditApp:mainfrom
objectiveous:fix/arrow-keys-after-font-change

Conversation

@objectiveous
Copy link

Description

After changing the font size, the down/up arrow keys stop moving the cursor across line boundaries. The root cause is that TextViewLayoutManager caches an estimated line height that isn't invalidated when the font changes. The moveDown:/moveUp: methods use this stale value to calculate the target point, which lands within the same line instead of the next one.

Fix:

  • Re-assign renderDelegate on the layout manager after font or line height multiplier changes, which forces the cached estimate to refresh.
  • Add explicit handleArrowKey dispatch so arrow keys with all modifier combinations (Shift, Option, Cmd) are handled correctly when the local event monitor intercepts them.
  • Add Control-N / Control-P support for Emacs-style vertical movement.

Related Issues

  • Related to cursor navigation after configuration changes

Checklist

  • I read and understood the contributing guide as well as the code of conduct
  • The issues this PR addresses are related to each other
  • My changes generate no new warnings
  • My code builds and runs on my machine
  • My changes are all related to the related issue above
  • I documented my code

The cached _estimateLineHeight in TextLayoutManager was never invalidated
when the font changed. The vertical cursor movement calculation uses this
estimate to compute the target y-coordinate, and after enough font size
increases the stale (too small) value prevented moveDown: from crossing
into the next line. Up arrow was unaffected because subtracting from the
line top always lands in the previous line.

Fix: re-assign renderDelegate after font/lineHeight changes to trigger
its didSet which clears the cached estimate. Also handle arrow keys
explicitly in the event monitor and add Ctrl+N/P (moveDown/moveUp)
to handleCommand for robustness.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@0xWDG 0xWDG requested review from Copilot and thecoolwinter and removed request for thecoolwinter February 8, 2026 14:42
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a cursor-navigation regression where vertical movement via Up/Down arrows stops crossing line boundaries after font size or line-height changes, by forcing the layout manager’s cached line-height estimate to refresh and by explicitly dispatching arrow-key handling when a local event monitor is installed.

Changes:

  • Refresh TextViewLayoutManager’s cached estimated line height after font and line-height multiplier updates.
  • Add explicit Up/Down arrow key dispatch (including common modifier combinations) in the local key event monitor path.
  • Add Emacs-style vertical movement bindings: Ctrl-N (down) / Ctrl-P (up).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
Sources/CodeEditSourceEditor/SourceEditorConfiguration/SourceEditorConfiguration+Appearance.swift Forces layout-manager cache refresh after font/line-height changes to restore correct vertical cursor navigation.
Sources/CodeEditSourceEditor/Controller/TextViewController+Lifecycle.swift Routes arrow keys through a dedicated handler and adds Ctrl-N/Ctrl-P vertical movement shortcuts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 90 to 94
// Force the layout manager to recalculate its cached estimated line height.
// The estimate is cached and not invalidated by font changes, causing vertical
// cursor movement (moveDown:) to use stale values and fail to cross line boundaries.
let renderDelegate = controller.textView.layoutManager.renderDelegate
controller.textView.layoutManager.renderDelegate = renderDelegate
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The layout-manager cache invalidation is implemented by reassigning renderDelegate to itself. This works but is duplicated (and relies on a non-obvious side effect). Consider extracting this into a small helper (e.g., refreshEstimatedLineHeightCache() on the controller/layout manager) so the intent is clearer and the logic is not repeated.

Copilot uses AI. Check for mistakes.
Comment on lines 111 to 113
// Also invalidate the cached estimated line height (same issue as font change above).
let renderDelegate = controller.textView.layoutManager.renderDelegate
controller.textView.layoutManager.renderDelegate = renderDelegate
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as the font-change branch above: the renderDelegate self-reassignment cache invalidation is duplicated. Please centralize this into a helper to avoid repeating the same workaround in multiple places.

Copilot uses AI. Check for mistakes.
Comment on lines +283 to +288
case (controlKey, "n"):
self.textView.moveDown(nil)
return nil
case (controlKey, "p"):
self.textView.moveUp(nil)
return nil
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Ctrl-N / Ctrl-P shortcuts match only when modifierFlags equals exactly .control. Because modifierFlags is built from .deviceIndependentFlagsMask, it can include extra flags (e.g. Caps Lock / Fn / numericPad), which would prevent these bindings from firing. Consider normalizing the flags to only [.shift, .control, .option, .command] before switching, or using .contains(.control) with explicit exclusions for other modifiers as needed.

Copilot uses AI. Check for mistakes.
Comment on lines 294 to 297
/// Handles up/down arrow key events with all modifier combinations.
/// Dispatches the appropriate movement method on the text view and consumes the event.
///
/// - Returns: `nil` to consume the event after dispatching the movement action.
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc comment says this handles arrow keys "with all modifier combinations", but the implementation only dispatches for Shift/Option/Command combinations and returns the event for others (e.g. Control+Arrow). Please adjust the wording to match what is actually handled so readers don’t assume broader coverage.

Suggested change
/// Handles up/down arrow key events with all modifier combinations.
/// Dispatches the appropriate movement method on the text view and consumes the event.
///
/// - Returns: `nil` to consume the event after dispatching the movement action.
/// Handles up/down arrow key events for plain, Shift, Option, Command,
/// and their combinations among these modifiers.
/// Dispatches the appropriate movement method on the text view and consumes the event.
///
/// - Returns: `nil` to consume the event after dispatching the movement action,
/// or the original event for unsupported modifier combinations.

Copilot uses AI. Check for mistakes.
///
/// - Returns: `nil` to consume the event after dispatching the movement action.
private func handleArrowKey(event: NSEvent, modifierFlags: NSEvent.ModifierFlags) -> NSEvent? {
let isDown = event.keyCode == 125
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

handleEvent defines downArrow/upArrow constants, but handleArrowKey re-hardcodes 125 to compute isDown. To reduce magic numbers and keep key-code mapping consistent, reuse the same constants (or centralize them) instead of duplicating the raw value.

Suggested change
let isDown = event.keyCode == 125
let downArrow: UInt16 = 125
let upArrow: UInt16 = 126
let isDown = event.keyCode == downArrow

Copilot uses AI. Check for mistakes.
objectiveous added a commit to objectiveous/CodeEditSourceEditor that referenced this pull request Feb 8, 2026
- Extract renderDelegate cache invalidation into refreshEstimatedLineHeightCache() helper
- Normalize modifier flags to [shift, control, option, command] so Ctrl-N/P work with Caps Lock/Fn
- Fix handleArrowKey doc comment to accurately describe supported modifier combinations
- Centralize key code constants (tab, downArrow, upArrow) as static properties

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract renderDelegate cache invalidation into refreshEstimatedLineHeightCache() helper
- Normalize modifier flags to [shift, control, option, command] so Ctrl-N/P work with Caps Lock/Fn
- Fix handleArrowKey doc comment to accurately describe supported modifier combinations
- Centralize key code constants (tab, downArrow, upArrow) as static properties

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant